feat(@typegpu/gl): Implicit pointer definitions in GLSL generator - #2819
Conversation
|
pkg.pr.new packages benchmark commit |
Resolution Time Benchmark---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [1.01, 2.01, 4.36, 6.42, 8.17, 11.44, 23.20, 23.43]
line [1.04, 1.99, 4.27, 6.56, 8.16, 11.54, 22.65, 24.02]
line [0.99, 2.07, 4.19, 6.42, 7.55, 11.92, 23.42, 23.87]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.28, 0.49, 0.68, 0.84, 1.12, 1.18, 1.40, 1.58]
line [0.32, 0.54, 0.75, 0.84, 1.16, 1.26, 1.53, 1.65]
line [0.30, 0.53, 0.70, 0.91, 1.17, 1.26, 1.54, 1.61]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.81, 2.12, 4.41, 6.63, 12.52, 26.65, 56.31, 111.15]
line [0.84, 2.20, 4.11, 7.04, 12.76, 26.61, 57.55, 114.69]
line [1.04, 2.22, 4.77, 6.60, 13.01, 27.16, 56.78, 118.90]
|
Bundle size comparison (
|
| 🟢 Decreased (max -0.28%) | ➖ Unchanged | 🔴 Increased (max 2.79%) | ❔ Unknown |
|---|---|---|---|
| 6 | 130 | 187 | 1 |
import * as ... in PR vs import * as ... in target (did bundle size increase?):
| Test | tsdown |
|---|---|
| std_isBeingTranspiled.ts | 15.71 kB ( |
| std_getTargetShaderLanguage.ts | 15.77 kB ( |
| std_getShaderStage.ts | 15.76 kB |
import { ... } in PR vs import * as ... in PR (is the library tree-Shakeable?):
| Test | tsdown |
|---|---|
| tgpu_init.ts | 270.45 kB ( |
| tgpu_initFromDevice.ts | 269.91 kB ( |
| tgpu_resolve.ts | 170.71 kB ( |
| tgpu_resolveWithContext.ts | 170.64 kB ( |
| tgpu_bindGroupLayout.ts | 73.82 kB ( |
| tgpu_mutableAccessor.ts | 68.55 kB ( |
| tgpu_accessor.ts | 68.54 kB ( |
| tgpu_privateVar.ts | 67.23 kB ( |
| tgpu_workgroupVar.ts | 67.23 kB ( |
| tgpu_const.ts | 66.65 kB ( |
| tgpu_lazy.ts | 66.45 kB ( |
| tgpu_fragmentFn.ts | 38.92 kB ( |
| tgpu_fn.ts | 38.87 kB ( |
| tgpu_vertexFn.ts | 38.74 kB ( |
| tgpu_computeFn.ts | 38.44 kB ( |
| tgpu_vertexLayout.ts | 27.57 kB ( |
| tgpu_comptime.ts | 15.18 kB ( |
| tgpu_unroll.ts | 1.75 kB ( |
| tgpu_slot.ts | 1.70 kB ( |
If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.
There was a problem hiding this comment.
Pull request overview
This PR adds GLSL-compatible handling for TypeGPU’s “implicit pointer” const-aliasing semantics, enabling the WebGL/GLSL fallback to preserve expected mutation behavior without native pointer support.
Changes:
- Refactors WGSL const-alias handling into an overridable
_aliasConstStatementhook. - Implements GLSL-specific aliasing rules: copy from immutable origins (e.g. uniforms), otherwise inline the aliased l-value expression and hoist runtime index expressions into temporaries to ensure single evaluation.
- Adds a dedicated
implicitPointer.test.tssuite covering immutable copies, mutable aliases, runtime index hoisting, and nested/alias-of-alias cases.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/typegpu/src/tgsl/wgslGenerator.ts | Extracts implicit-pointer const-alias logic into _aliasConstStatement for reuse/override. |
| packages/typegpu-gl/src/glslGenerator.ts | Overrides const-alias lowering to be pointer-free in GLSL via copy-or-alias + index-hoisting. |
| packages/typegpu-gl/tests/implicitPointer.test.ts | Adds snapshot tests validating GLSL output for implicit pointer scenarios. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Important
The substitution + index-hoisting design is sound and matches WGSL pointer semantics in the cases I traced, but the immutable branch copies aliased values via _emitVarDecl, which emits invalid GLSL ES 3.00 whenever the aliased value is array-typed (const a = u.arr; → float a[4] = u.arr;, which GLSL ES 3.00 does not allow). See the inline comment for the fix.
Reviewed changes
- GLSL alias const statements —
_aliasConstStatementoverride inglslGenerator.ts: copies aliases of immutable memory (uniform/readonly/handle) into locals, and turns mutable-memory aliases into compile-time substitutions of the aliased expression, hoisting non-constant index expressions into once-evaluated temp variables. - Index hoisting helper — new
#hoistIndexAccesseswalks member/index accesses and replaces runtime index expressions with@index_Nreferences tied to freshly declared temps; comptime-constant indices are bound directly. - WGSL generator refactor — the implicit-pointer const handling in
wgslGenerator.tswas extracted into a protected_aliasConstStatementhook with line-identical logic (makeUniqueIdentifier(rawId, 'block')preserved); WGSL generator/constant/struct tests still pass (139 tests). - New test suite —
implicitPointer.test.tswith six snapshot tests (immutable copy, mutable substitution, runtime-index hoisting, nested member/index chains, alias-of-an-alias, local-variable alias).
I verified the edge semantics directly: index expressions are snapshotted at the declaration (later reassignment of the index variable doesn't affect the alias, matching WGSL pointer-address semantics), and alias-of-alias chains resolve correctly through the stored expressions.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
12cf3a1 to
1e1ea6e
Compare
1e1ea6e to
339c509
Compare
339c509 to
8715ec1
Compare
8715ec1 to
abec4f8
Compare
abec4f8 to
f8f2a8b
Compare
f8f2a8b to
e36ebd2
Compare
e36ebd2 to
909d278
Compare
c951b6e to
27fd9a1
Compare
There was a problem hiding this comment.
ℹ️ No critical issues — one stale doc example to fix.
The rework since the last review is clean: dropping the @index_N placeholder scheme in favor of real idx-prefixed temps (unique via makeUniqueIdentifier('idx', 'block')), inlining comptime-constant indices guarded by !possibleSideEffects, and removing undecorateDataType from both branches. I verified the undecorateDataType removal is safe — @location-decorated fields still emit plain GLSL (vec3 p = u.pos;), consistent with the WGSL base _aliasConstStatement which also uses eq.dataType directly. The new multi-index test pins the single-evaluation semantics well.
Reviewed changes
- Reworked
#hoistIndexAccessesto substitute comptime-constant indices inline (now guarded by!index.possibleSideEffects) and to hoist runtime indices into once-evaluatedidxtemps, removing the@index_Nplaceholder scheme and the#hoistedIndexCountcounter entirely. - Dropped
undecorateDataType(...)from both the immutable-copy and mutable-alias branches of_aliasConstStatementso the alias type flows through undecorated, matching the WGSL base treatment. - Renamed hoisted temps
item→idxacross the generator and the snapshot suite, and added ahoists multiple index accessedtest covering nested 2D indices (boids.$[index]![idx2]!) plus a user variable namedidx2.
Technical details
The constant-index inlining branch now returns [NODE.indexAccess, target, node[2]] (the raw AST index) instead of binding a placeholder snippet; re-_expression of a comptime constant is deterministic, and the !possibleSideEffects guard makes the path strictly more conservative than before. Runtime indices register via defineVariable(name, snip(name, ...)), which is a self-referential identity that still lets makeUniqueIdentifier reserve the name — sound.
ℹ️ Nitpicks
- Test description
'hoists multiple index accessed'— should read'hoists multiple index accesses'. - The
@exampleon#hoistIndexAccessesstill shows the olditem/item_1naming, which no longer matches emitted code.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| * | ||
| * @example | ||
| * ``` | ||
| * arr[foo()].prop[idx] => arr[item].prop[item_1] |
There was a problem hiding this comment.
The @example is stale after the item → idx rename: the emitted temps are now named via makeUniqueIdentifier('idx', 'block') (e.g. idx, idx_1), so the documented arr[item].prop[item_1] / 'int item = foo();' no longer matches actual output. The example's input variable idx also now collides conceptually with the generated temp prefix.
| * arr[foo()].prop[idx] => arr[item].prop[item_1] | |
| * arr[foo()].prop[j] => arr[idx].prop[idx_1] | |
| * // out: ['int idx = foo();', 'int idx_1 = j;'] |
27fd9a1 to
0760dff
Compare
0760dff to
19f5524
Compare

Closes #2679